package com.ymt.bpm.util.w3cdom;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.CDATASection;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class XmlDomUtil {
	
	/**
	 * Add a Datarow[] array to the existing datarow list of the document, does NOT override existing ones.
	 * @param doc
	 * @param datarow
	 */
	public static void addDataRow(Document doc, Node rootElem, DataRow... datarow) {
		List<DataRow> datarowList = Arrays.asList(datarow);
		addDataRow(doc, rootElem, datarowList);
	}
	
	/**
	 * Add a Datarow list to the existing datarow list of the document, does NOT override existing ones.
	 * @param doc
	 * @param datarows
	 */
	public static void addDataRow(Document doc, Node rootElem, List<DataRow> datarows) {
		if (datarows==null || datarows.size()==0) {
			return;
		}
		// add datarow
		addDataRowToElement(doc, rootElem, datarows);
	}
	
	/**
	 * Utility method, used for transform a w3c XML document to String, removes the XML declaration.
	 * @param doc Input w3c XML document.
	 * @return String format of the XML document
	 * @throws TransformerException
	 */
	public static String transformXmlToString(Document doc) throws TransformerException {
		return transformXmlToString(doc, true);
	}
	
	/**
	 * Utility method, used for transform a w3c XML document to String.
	 * @param doc Input w3c XML document.
	 * @param removeDeclaration remove the XML declaration if true.
	 * @return String format of the XML document
	 * @throws TransformerException
	 */
	public static String transformXmlToString(Document doc, boolean removeDeclaration) throws TransformerException {
		// create the transform source
		Source source = new DOMSource(doc.getDocumentElement());
		
		//Set the output for the transformation
		StringWriter responsesStrWriter = new StringWriter();
		StreamResult stream = new StreamResult(responsesStrWriter);
		
		//transform
		SAXTransformerFactory transformerFactory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
		Transformer transformer = transformerFactory.newTransformer();
		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, removeDeclaration ? "yes" : "no");
		transformer.transform(source, stream);
		responsesStrWriter.flush();	//flush
		
		//return
		return responsesStrWriter.toString();
	}
	
	/**
	 * Remove XML declaration in a string format XML.
	 * @param xmlString
	 * @return result string after removing the XML declaration.
	 * @throws TransformerException
	 */
	public static String removeXMLDeclarationInString(String xmlString) throws TransformerException {
		
		// create the transform source
		StreamSource source = new StreamSource(new StringReader(xmlString));
		
		//Set the output for the transformation
		StringWriter responsesStrWriter = new StringWriter();
		StreamResult stream = new StreamResult(responsesStrWriter);
		
		//transform
		SAXTransformerFactory transformerFactory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
		Transformer transformer = transformerFactory.newTransformer();
		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
		transformer.transform(source, stream);
		responsesStrWriter.flush();	//flush
		
		//return
		return responsesStrWriter.toString();
	}
	
	/**
	 * Get the value of first element that found by name.
	 * @param doc The w3c XML document where to find the element
	 * @param elementName The element name, such as 'PARTNER', if there is a name space prefix, you must specify it, for example, 'ns1:PARTNER'
	 * @return The value of first element found by name, returns null if no element found.
	 */
	public static String getElement(Document doc, String elementName) {
		NodeList list = doc.getElementsByTagName(elementName);
		if (list.getLength()==0) {
			return null;
		}
		String value = list.item(0).getTextContent();
		return value;
	}
	
	/**
	 * Get values of all elements found by the same name, the result is a list.
	 * @param doc The w3c XML document where to find the element
	 * @param elementName The element name, such as 'PARTNER', if there is a name space prefix, you must specify it, for example, 'ns1:PARTNER'
	 * @return The value list of elements found by the same name, returns null if no element found.
	 */
	public static List<String> getElementList(Document doc, String elementName) {
		NodeList list = doc.getElementsByTagName(elementName);
		if (list.getLength()==0) {
			return null;
		}
		List<String> rtnList = new ArrayList<String>();
		for (int i=0; i<list.getLength(); i++) {
			rtnList.add(list.item(i).getTextContent());
		}
		return rtnList;
	}
	
	/**
	 * Build a XML document from string, please always consider method 'newRequestXml' first.
	 * @param xmlString
	 * @return
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 */
	public static Document buildXmlFromString(String xmlString) throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setNamespaceAware(false);
		DocumentBuilder db = factory.newDocumentBuilder();
		Document doc = db.parse(new InputSource(new StringReader(xmlString)));
		return doc;
	}
	
	/**
	 * Get nodes by XPATH.
	 * @param sourceXmlStr
	 * @param ctx
	 * @param expression
	 * @return
	 * @throws XPathExpressionException
	 * @throws IOException
	 */
	public static NodeList getNodesByXPath(String sourceXmlStr, NamespaceContext ctx, String expression) throws XPathExpressionException, IOException {
		XPathFactory f = XPathFactory.newInstance();
		XPath xpath = f.newXPath();
		xpath.setNamespaceContext(ctx);
        XPathExpression ex = xpath.compile(expression);
        NodeList nl = (NodeList) ex.evaluate(new InputSource(new StringReader(sourceXmlStr)), XPathConstants.NODESET);
        return nl;
	}
	
	/**
	 * Recursively add data row.
	 * @param doc
	 * @param rootElem
	 * @param datarows
	 */
	public static void addDataRowToElement(Document doc, Node parentElem, List<DataRow> datarows) {
		if (parentElem==null) {
			parentElem = doc.getDocumentElement();
		}
		addDataRowToElement(doc, parentElem, datarows, false, null);
	}
	
	/**
	 * Recursively add data row.
	 * @param doc
	 * @param rootElem
	 * @param datarows
	 */
	public static void addDataRowToElement(Document doc, Node parentElem, List<DataRow> datarows, boolean insertBefore, Node refElement) {
		
		if (parentElem==null) {
			parentElem = doc.getDocumentElement();
		}
		
		for (DataRow datarow : datarows) {
			
			// create DATAROW element
			Element row = doc.createElement(datarow.getLocalName());
			
			// set attribute
			if (datarow.getAttributes() != null && datarow.getAttributes().size()>0) {
				for (String attrName : datarow.getAttributes().keySet()) {
					row.setAttribute(attrName, datarow.getAttributes().get(attrName));
				}
			}
			
			// set elements
			boolean hasElement = false;
			if (datarow.getElements() != null && datarow.getElements().size()>0) {
				hasElement = true;
				for (String elemName : datarow.getElements().keySet()) {
					Element rowElem = doc.createElement(elemName);
					rowElem.setTextContent(datarow.getElements().get(elemName));
					row.appendChild(rowElem);
				}
			}
			
			// add to document
			if (insertBefore) {
				parentElem.insertBefore(row, refElement);
			} else {
				parentElem.appendChild(row);
			}
			
			// set child data rows
			boolean hasChild = false;
			if (datarow.getChildren() != null && datarow.getChildren().size() > 0) {
				//Element childrenElem = doc.createElement(datarow.getChildrenTagName());
				//row.appendChild(childrenElem);
				hasChild = true;
				addDataRowToElement(doc, row, datarow.getChildren());
			}
			
			//set text content
			if (datarow.getTextContent() != null && datarow.getTextContent().length()>0) {
				if (hasElement || hasChild) {
					throw new DOMException(DOMException.NO_DATA_ALLOWED_ERR, "DataRow must not contain textContent while having child elements.");
				}
				if (datarow.isUseCDATAForTextContent()) {
					CDATASection cdata = doc.createCDATASection(datarow.getTextContent());
					row.appendChild(cdata);
				} else {
					row.setTextContent(datarow.getTextContent());
				}
			}
		}
	}
	
}
